Technote Number 10XX (DRAFT)

Writing a USB Driver

By Rich Kubota
Apple Worldwide Developer Technical Support

CONTENTS

USB Driver Overview

Mac OS USB Class Driver Basics

TheUSBDriverDescription Sample

TheClassDriverPluginDispatchTable

Difference between Device and Interface Drivers

Handling Hot Unplugging

Communicating With Client Processes

Detecting Device Presence

Mac OS USB Compatibility With Mac OS Toolbox Calls

Mac OS USB Notes

Summary

This technote is a work-in-progress document. Please see the readme file, if you have not done so already.

This technote describes how to write a USB Class Driver, and discuss how to make it possible for Mac OS processes to communicate with the device via the class driver.

The section, USB Class Driver Basics, provides a description of how the USB Manager matches a USB device to a class driver, and how the USB Manager works with the class driver.

The sections, TheUSBDriverDescription Sample and TheClassDriverPluginDispatchTable, describe in more detail the two important symbols that a class driver must export and how it uses the information to work with the device.

The section Communicating With Client Processes describes an "Interface Shim" and provides an overview of the role that it plays in handling communications between Mac OS processes and the class driver.

The Mac OS USB Notes section provides additional information that will be of interest to USB developers.

USB Driver Overview

The USB Class Driver provides the software interface between USB hardware and client processes that communicate with the hardware. This document describes how the USB Manager interacts with class drivers when a USB device is recognized, and the basic functionality that the driver must implement. Important issues for the class driver include, handling the hot unplug situation, and how existing Macintosh processes communicate with the Class Driver.

This document is targeted to developers of USB devices to understand how USB interacts with their device, and what software components need to be implemented. Apple provides class drivers for hub devices, and mouse and keyboard HIDs (Human Interface Device). As of the USB 1.0 release, unless specifically supported by Apple, the developer will need to implement a class driver for their device. One other important component to consider is a software "shim" which will make it possible for existing processes to communicate with the USB device.

For USB gaming devices, there is an alternate mechanism to consider - the InputSpocket SDK v1.3. InputSprockets v1.3 works with the UniversalHIDModule to recognize your device. Instead of writing a class driver, you provide an InputSprocket library.

 

Mac OS USB Class Driver Basics

In this section, we provide an overview of the Mac OS USB startup process and how USB interacts with a class driver module. It is assumed that the reader is familiar with the PCI Driver fundamentals as documented in the "Designing PCI Cards and Drivers". This document is included with the PCI Driver 2.0" Driver Development Kit (DDK).

A Mac OS USB Class Driver is implemented as a Shared Library with a file type 'ndrv'. As with other PCI drivers, the CFM code fragment must export driver description structures to characterize the USB functionality. All USB Class drivers are recognized by theUSBDriverDescription record. The use of theUSBDriverDescription record is modeled after the implementation of the DriverDescription record, which is discussed in the document " Final Designing PCI Cds&Drivers". This document is a part of the PCI Driver 2.0" Driver Development Kit (DDK).

The Mac OS USB Manager handles the recognition of all connected USB devices, both at startup and when hot-plugged. As part of device recognition, Mac OS USB assigns the unique USB address, and opens a control pipe to endpoint zero of the device. Mac OS USB queries the device's endpoint 0 for the device descriptor, which includes information like vendor id, product id, device class,subclass, and protocol.

Upon receipt of the standard information, Mac OS USB searches for all USB Class Drivers, which are CFM code libraries of file type 'ndrv'. For each library, the USB Manager looks for the exported symbol "TheUSBDriverDescription" and subsequently at the first four bytes of the structure to match the kTheUSBDriverDescriptionSignature. This identifies the CFM code fragment as a USB Class Driver. Refer to the section "TheUSBDriverDescription Sample" for more information and an example of this structure.

Matching Devices to Drivers

Mac OS USB defines a match criteria to determine which class driver is selected to support a USB device. A match value is assigned to each USB driver. The following criteria determine the match value under Mac OS USB v1.0. Future versions of Mac OS USB may specify additional match criteria. The following match criteria are listed from highest to lowest

  1. vendor and product IDs match. A match of the usbDeviceReleaseNumber increases the match value.
  2. device class vendor specific, match of vendor ID, device subclass and protocol.
  3. kUSBDoNotMatchGenericDevice bit clear in the usbDriverLoadingOptions field of driver, match of device class, device subclass and device protocol.
  4. kUSBDoNotMatchGenericDevice bit clear in the usbDriverLoadingOptions field of driver, match of device class, device subclass and kUSBProtocolMustMatch bit clear in the usbDriverLoadingOptions field of driver.

A driver can set the kUSBDoNotMatchGenericDevice bit in the usbDriverLoadingOptions field of the "TheUSBDriverDescription" so that the driver can only be considered for supporting vendor specific devices. If the bit is clear, then the driver indicates generic support for devices by device class and subclass. Unless you plan to write a class module which will support all devices with a specific class and subclass, you should have your device return a device class of 0xFF (vendor specific) and set the kUSBDoNotMatchGenericDevice bit in the usbDriverLoadingOptions field of the USBDriverDescription.

When a match, or "best match" is found, USB registers the device in the Name Registry. The TheClassDriverPluginDispatchTable is accessed, and the Name Registry is updated to reflect that presence of the new device. Refer to the section below on "TheClassDriverPluginDispatchTable Sample" for more information on this structure. The USB Manager calls the validateHWProcPtr to have the class driver verify that the driver supports the hardware. Then the initializeDeviceProc routine is called so that the class driver can establish communications with the device. Refer to the sections below "validateHWProc Function" and "initializeDeviceProc Function" below for more information on implementing these functions.

For USB devices that match to the Composite Class type (device class 0, subclass 0), the Composite Class Driver will be called, which in turn detects the presence of available configurations. The Composite Class Driver will set the appropriate configuration based on the power and or bandwidth available, and make the USBExpertInstallInterfaceDriver call. The USB Manager identifies the Interface class, subclass and protocol, and rescans the USB Class Drivers for a match of the Interface Descriptor section of the driver description. When a match (or best match) is found, the USB Manager loads the code fragment, and finds the TheClassDriverPluginDispatchTable.

Matching Interfaces to Interface Drivers

Similar to matching devices with drivers, Mac OS USB defines a match criteria to determine which class driver is selected to support a USB interface. A match value is assigned to each USB driver. The following criteria determine the match value under Mac OS USB v1.0. Future versions of Mac OS USB may specify additional match criteria. The following match criteria are listed from highest to lowest

  1. vendor and product IDs match along with the interface number. A match of the usbDeviceReleaseNumber increases the match value by 1.
  2. interface class vendor specific, match of vendor ID and device subclass. A match of the protocol increases the match value by 1.
  3. kUSBDoNotMatchGenericDevice bit clear in the usbDriverLoadingOptions field of driver, match of interface class, interface subclass and interface protocol.
  4. kUSBDoNotMatchGenericDevice bit clear in the usbDriverLoadingOptions field of driver, match of interface class, interface subclass,.and kUSBProtocolMustMatch bit clear in the usbDriverLoadingOptions field of driver.
  5. kUSBDoNotMatchInterface bit set in the usbDriverLoadingOptions field of driver - don't match driver for interface.

Once USB has matched a class driver to the interface, the initializeInterfaceProc is called, for which the class driver prepares itself to communicate with the interface. As with device matching, you will likely want to match interfaces by vendor and product ID or by having the interface class set to 0xFF (vendor specific), plus set the kUSBDoNotMatchGenericDevice bit in the usbDriverLoadingOptions field so that the driver will not support the class and subclass interface generically.

The finalizeProc is called by the USB Manager, when the device has been removed. This function will be called at task time so that the Class Driver can clean up memory allocations, and perform other functions associated with the driver or interface going away. The finalizeProc must be synchronous. That is, when it returns kUSBNoErr, it is assumed that there is no further need to keep the driver code fragment around, and USB will unload it from memory.

 


TheUSBDriverDescription Sample

TheUSBDriverDescription is a required exported symbol that the USB Manager uses to first identify USB class drivers, and second, to identify whether a driver supports different USB devices. The structure of the USBDriverDescription record is as follows:

struct USBDriverDescription 
{
  OSType 		  usbDriverDescSignature;	
				/* Signature field of this structure. */
  USBDriverDescVersion usbDriverDescVersion;		
				/* Version of this data structure.    */
  USBDeviceInfo 	  usbDeviceInfo;				
				/* Product & Vendor Info              */
  USBInterfaceInfo     usbInterfaceInfo;			
				/* Interface info                     */
  USBDriverType 	  usbDriverType;				
				/* Driver Info.                       */
  USBDriverLoadingOptions usbDriverLoadingOptions;	
				/* Options for class driver loading.  */
};
typedef struct USBDriverDescription		USBDriverDescription;

Set the usbDriverDescSignature to kTheUSBDriverDescriptionSignature to indicate that the CFM library is a USB class driver.

The second field of the DriverDescription is the structure version field. As of the initial release of the DDK, this field should be set to kInitialUSBDriverDescriptor. For future releases of Mac OS USB, you may need to set this field to indicate support for new functionality.

The remaining fields of the driver description record are used to match the class driver to a device or interface. For most of the fields, a value of zero indicates that the field is not to be used to match the class driver to a device. For other fields such as the usbDeviceClass and usbInterfaceClass, a device and interface specific value must be entered.

USBDeviceInfo Structure

The information presented in the USBDeviceInfo structure is used to match a class driver to support both device drivers and interfaces. The kUSBDoNotMatchInterface bit must be clear in the USBDriverLoadingOptions field in order for the class driver to be checked to see if it can support an interface. As indicated above for device and interface matching, the highest match value is generated when the VendorID and ProductID match. For an interface match, the interface number must also match.

The USBDeviceInfo structure is defined as follows:

struct USBDeviceInfo {
  UInt16 usbVendorID;		 /* USB Vendor ID            */
  UInt16 usbProductID;	      /* USB Product ID           */
  UInt16 usbDeviceReleaseNumber;  /* Release Number of Device */
  UInt16 usbDeviceProtocol;	 /* Protocol Info.           */
};

For the USBDeviceInfo fields, VendorID and ProductID, enter the values that correspond to the device that the class driver will support. For the DeviceReleaseNumber field, enter a BCD value for the device version. For the protocol field, enter the protocol value assigned by USB. A value of 0 indicates that no device specific protocol is used and is a valid setting for the device. A value of 0xFF indicates that a vendor specific protocol is used. A match of the VendorID and the ProductID is the highest possible match. Specific information about driver matching is presented above.

USBInterfaceInfo Structure

The information presented in the USBInterfaceInfo structure is used to match a class driver to support a device interface. When a device is detected as a composite class device, the Composite Class Driver is loaded and will try to match class a class driver for each interface. For interface matching, the second highest match value is generated when the interface class is vendor specific AND the vendorID and interface subclass match. In case of duplicate matches, matching the interface protocol field for a driver raises its match level.

A class driver can support an interface class genericly by not setting the kUSBDoNotMatchGenericDevice bit in the USBDriverLoadingOptions field. When this bit is clear, USB checks the class module's support for the interface class, subclass and protocol against that for the device. When all three values match, the third highest match level is set for the driver. If the class and subclass match and the kUSBProtocolMustMatch bit is clear, then the fourth highest match level is set for the driver.

The USBInterfaceInfo structure is defined as follows:

struct USBInterfaceInfo {
  UInt8 usbConfigValue;	  /* Configuration Value */
  UInt8 usbInterfaceNum;	 /* Interface Number    */
  UInt8 usbInterfaceClass;     /* Interface Class     */
  UInt8 usbInterfaceSubClass;  /* Interface SubClass  */
  UInt8 usbInterfaceProtocol;  /* Interface Protocol  */
};

USBDriverType Structure

The information provided in the USBDriverType structure is used to match a class driver to support a device. For device matching, the second highest match level occurs when the device class is vendor specific, 0xFF, and the vendorID in the USBDeviceInfo field, the device subclass and protocol fields all match.

A class driver can support a device class genericly by not setting the kUSBDoNotMatchGenericDevice bit in the USBDriverLoadingOptions field. When this bit is clear, USB checks the class module's support for the device class, subclass and protocol against that for the device. When all three values match, the third highest match level is set for the driver. If the class and subclass match and the kUSBProtocolMustMatch bit is clear, then the fourth highest match level is set for the driver.

The USBDriverType structure is defined as follows:

struct USBDriverType {
  Str31 	nameInfoStr;       /* Driver's name when loading into  */														the Name Registry.*/
  UInt8 	usbDriverClass;    /* USB Class this driver belongs to.*/
  UInt8 	usbDriverSubClass; /* Module type                      */
  NumVersion  usbDriverVersion;  /* Class driver version number.     */
};

The nameInfoStr field, is used by the USB Expert to record debugging information with. The string is Pascal with the length bit in byte 0. The usbDriverClass and usbDriverSubClass fields are used as described above to match a class module genericly to a device. In a future release of USB, the usbDriverVersion field will be used to distinguish class module drivers.

USBDriverLoadingOptions Field

The USBDriverLoadingOptions field is used to control whether a class driver will support generic devices, interfaces, and other matching options. As of the v1.0 release, the following options are defined.

The kUSBDoNotMatchGenericDevice bit indicates whether the class driver supports or not, all devices or interfaces. When set, the class module can only support a device with a matching vendorID as specified above. If this bit setting is clear, then the class module can support a device generically as specified above.

kUSBDoNotMatchGenericDevice = 0x00000001

The kUSBDoNotMatchInterface bit indicates whether the class driver supports USB interfaces. When set, Mac OS USB will not consider the driver for consideration in support of an interface. If this bit setting is clear, then the class module indicates that it can support an interface per settings in the USBInterfaceInfo structure.

kUSBDoNotMatchInterface	= 0x00000002

The kUSBProtocolMustMatch bit indicates whether a generic class driver can match when the class and subclass values match, but the protocol does not match. Set this bit in to require that USB match the protocol field, in addition to the class and subclass values during driver matching. Otherwise, leave this bit clear and a match value is generated when the class and subclass match.

kUSBProtocolMustMatch	  = 0x00000004

 

The following USBDriverDescriptor is from the mouse sample. This driver sample provides generic support for mice that are not support by a vendor specific class driver.

USBDriverDescription TheUSBDriverDescription = {
	// Signature info
  kTheUSBDriverDescriptionSignature,  // specifies USB Class Device Driver
  kInitialUSBDriverDescriptor,        // specifies the USB Class Driver version
	
	// Device Info
  0,			      // vendor, 0 = unspecified
  0,			      // product, 0 = unspecified
  0,			      // version of product, 0 = unspecified
  kUSBMouseInterfaceProtocol,   // protocol, 0 = not device specific
	
	// Interface Info				
  0,			      // Configuration Value, 0 = unspecified
  0,			      // Interface Number, 0 - interface 0
  kUSBHIDInterfaceClass,	// Interface Class, 0x03 - HID Interface
  kUSBBootInterfaceSubClass,   // Interface SubClass, 0x01 - boot interface subclass
  kUSBMouseInterfaceProtocol,  // Interface Protocol, 0x02 - mouse HID interface
		
	
	// Driver Info
  "\pUSBHIDMouseModule",	// Driver name for Name Registry
  kUSBHIDInterfaceClass,	// Device Class, 0x03 HID Interface class
  kUSBBootInterfaceSubClass,   // Device Subclass, 0x01 boot interface subclass
  kMouseHexMajorVers, 
  kMouseHexMinorVers, 	
  kMouseCurrentRelease, 
  kMouseReleaseStage,	   // version of driver
	
	// Driver Loading Info
  kUSBProtocolMustMatch	// Flags  0x4 = the protocol must match
                                       will match to a generic device and interface
                                       will match to an interface
};

TheClassDriverPluginDispatchTable

TheClassDriverPluginDispatchTable is the second exported symbol which the CFM class driver module must export. USB looks for this symbol in order to initialize the class module. The structure of TheClassDriverPluginDispatchTable is as follows

struct USBClassDriverPluginDispatchTable {
  UInt32 pluginVersion;
  USBDValidateHWProcPtr validateHWProc;
      /* Proc for driver to verify proper HW        */
  USBDInitializeDeviceProcPtr initializeDeviceProc;
      /* Proc that initializes the class driver.    */
  USBDInitializeInterfaceProcPtr initializeInterfaceProc;
      /* Proc that initializes a particular interface in the class driver.*/
  USBDFinalizeProcPtr finalizeProc;
      /* Proc that finalizes the class driver.      */
  USBDDriverNotifyProcPtr notificationProc;
      /* Proc to pass notifications to the driver.  */
};

You must initialize the pluginVersion field to kClassDriverPluginVersion defined in the header file USB.h. The USB Manager uses this field to distinguish between future versions of the dispatch table.

The following fields of the dispatch table are required when the device is initialized

If any of these fields are nil, then the kUSBBadDispatchTable error is returned to the USBFamilyExpert, and the driver load fails. Each of these calls will be made at task time, and each is made synchronously.

You are advised to install a notificationProc, so that you can handle a "hot unplug" situation. However, if this field is nil, the driver load will still continue.

If your class driver can be used to communicate with a specific interface portion of a composite device, the following fields of the dispatch table are required.

If either of these fields are nil, then the kUSBBadDispatchTable error is returned, and the interface load fails. Each of these calls will be made at task time, and each is made synchronously.

As above, you are advised to install a notificationProc, so that you can handle a "hot unplug" situation. However, if this field is nil, the interface load will still continue.

The ValidateHWProc Function

The validateHWProc is called for the Class Driver to validate that the device is supported by the selected driver. The prototype for the validateHWProc is

OSStatus USBDValidateHWProcPtr(USBDeviceRef device, 
                               USBDeviceDescriptorPtr pDesc);

The USB Manager finds the best matching class driver for a device, then calls the USBValidateHWProc procptr to provide the class driver with a chance to check the hardware itself to verify that the device is supported. This procedure call is made at System Task time, and is issued synchronously. The class driver may want to verify that a certain minimum device hardware version exists, that a configuration string is present, or lock out support for specific incompatible products or versions.

Return a kUSBNoErr OSStatus result, if the Class Driver supports the hardware. Return a value other than kUSBNoErr to indicate that the driver does not support the hardware. The USBValidateHWProc should be solely used to validate the presence of the hardware. It should not be used to set up the driver connection. This functionality should be handled by the InitializeDeviceProc, which is discussed next.


The InitializeDeviceProc Function

USB calls the InitializeDeviceProc function to tell the class driver to prepare for communications with the USB device. The InitializeDeviceProc routine is called at System Task Time. The class driver allocates memory, sets up resources (see "Compatibility With Mac OS Toolbox" section below), and makes the necessary sequence of USB calls to configure the device. This will include identifying the available configurations, specifying the interface to be used, opening endpoints and pipes, specifying the protocol to be used, installing an interrupt routine, and related actions.

The following applies in general for both cases when the Class Module is called through the initializeDeviceProc and the initializeInterfaceProc. Both of these functions are called synchronously and at System Task time. There is no error returned. Before returning from the function, the driver must complete any calls that must be made at task time. After doing so, the driver should initiate an asynchronous "state machine process" to complete the connection. This can include memory allocation, assuming that the USBAllocMem call is made. Once the asynchronous state machine process is begun, the driver can return from the InitializeDeviceProc function call.

As a reminder, most USB calls are asynchronous. Per the USB Reference Guide, the USB driver can NOT poll for completion of these asynchronous USB calls by simply checking the usbStatus field without using a completion routine.

All of the USB examples class drivers implement an asynchronous state machine startup process. In the examples, the last thing that the InitializeDeviceProc (or InitializeInterfaceProc) code does before returning to the caller, is to make an InitiateTransaction call. The InitiateTransaction call uses the usbRefCon field as a selector into a switch statement. After the first call to InitiateTransaction is intiated, control returns to the caller. As the asynchronous call completes, the completion routine checks the call results, modifies the usbRefCon field appropriately and issues a call to InitiateTransaction. InitiateTransaction checks the usbRefCon field and issues a new asynchronous call to handle the next step in the state machine. This continues until all of the processing is complete.

A sample InitiateTransaction call for a bulk input/output USB device, might be as follows - note that error checking has been removed.

Sample InitiateTransaction function for Bulk IO device

void DeviceInitiateTransaction(USBPB *pb)
{
  int	     length;
  OSStatus	err;
  ..
   switch(pb.usbRefcon)
  {
			
    case kGetFullConfiguration:
	GetConfigurationDescriptor(pb);
	break;
     case kSetInterface:
	  //
	  //  having identified the configuration
	  //  set the device to our prefered interface
        //
	SetInterface(pb, interfaceNumber);
	break;
     case kGetInterface:
	  // failsafe check that we've got the right setup
	  // it's possible the device didn't respond to our SetInterface
	GetInterface( &pb, &whichInterface );
	break;
     case kOpenBulkOutPipe:
	  //
	  //  open mandatory bulk out pipe
	  //
	OpenBulkEndpoint( pb, kOpenBulkOutPipe );
	break;
     case kOpenBulkInPipe:	
	  //
	  //  open optional bulk in pipe
	  //
	OpenBulkEndpoint( pb, kOpenBulkInPipe );
	break;
     case kReturnFromDriver:
      break;
     default:
	USBExpertFatalError(device, kUSBInternalErr, "InitiateTransact failed",0);
	break;
  }
}
 

A sample completion routine for the sample bulk IO USB device, is as follows. Note that much of the error checking has been removed. As each call completes, assuming that there is no error, then the usbRefCon is advanced. The InitiateTransaction call is made, and a new asynchronous USB call is made.

Sample Completion Proc for Bulk IO Device

void DeviceCompletionProc(USBPB *pb)
{
   if((pb.usbStatus != noErr) && (pb.usbStatus != kUSBPending))
  {
    USBExpertStatus(device, "\p"  "retry", pb.usbStatus);
  }
     //
    //	advance to the next state
    //
  switch(pb.usbRefcon)
  {
    case kGetConfigurationDescriptor:
	if (pb.usbStatus == noErr )
	{
	  pb.usbRefcon = kGetFullConfiguration;	
	}
	break;
			
    case kGetFullConfiguration:
	if (pb.usbStatus == noErr )
	{
	  interface = FindInterface(config, kUSBClassProtoBidirectional );
	  if (interface == NULL )
	  {
	    interface = FindInterface(config, kUSBClassProtoUnidirectional );
	    if ( interface == NULL )
		USBExpertFatalError(device, kUSBInternalErr, …);
	  }
 	    //
	    // if there's only one interface (or alternate) supported
	    // skip the attempt to assign the interface
	    //
	  numInterface = CountInterface( config, kUSBClass, kUSBSubClass );
				
	  pb.usbRefcon = kSetInterface;
	}
	break;
			
    case kSetInterface:
	if (pb.usbStatus == noErr )
	{
	  pb.usbRefcon = kGetInterface;
	}
	break;
			
    case kGetInterface:
	if (pb.usbStatus == noErr )
	{
	  pb.usbRefcon = kOpenBulkOutPipe;
	}
	break;
			
    case kOpenBulkOutPipe:
	if (pb.usbStatus == noErr )
	{
	  if (interface->interfaceProtocol == kUSBClassProtocolBidirectional )
	    pb.usbRefcon = kOpenBulkInPipe;
	  else
	    pb.usbRefcon = kEnterNameRegistry;
	}
	break;
			
    case kOpenBulkInPipe:
	if (pb.usbStatus == noErr )
	{
	  readPipe = pb.usbReference;
 	  pb.usbRefcon = kInstallShim;
	}
	break;
			
    case kNilCompletion:
    default:
	if (pb.usbStatus == noErr )
	{
        pb.usbRefcon = kUndefined | kReturnFromDriver;
	}
	break;
  }
   if (pb.usbStatus == noErr )
  {
    if (!(pb.usbRefcon & kReturnFromDriver))
	DeviceInitiateTransaction(pb);
  }
  else
  {
    USBExpertFatalError(device, pb.usbStatus, StateStr(pb.usbRefcon, kPString), pb.usbRefcon);
    USBExpertFatalError(device, pb.usbStatus, USBStatusStr(pb.usbStatus, kPString), 0);
  }
}

Sample InitiateTransaction function for HID device

For a HID device, the initiateTransaction sample code would be as follows. Note that this sample code is a heavily edited version from the KeyboardModule.c file.

void HIDModuleInitiateTransaction(USBPB *pb)
{
  OSStatus myErr;
  switch(pKeyboardPB->pb.usbRefcon & ~kRetryTransaction)
  {
    case kSetKeyboardLEDs:
      …			
      myErr = USBDeviceRequest(&pKeyboardPB->pb);
      if(immediateError(myErr))
      {
	  // report error and quit 
      }
      break;
     case kGetFullConfiguration:
	…
	myErr = USBGetFullConfigurationDescriptor(pb);
	if(immediateError(myErr))
	{
  	// report error and quit 
	}
	break;
			
    case kFindInterfaceAndSetProtocol:
	myErr = FindHIDInterfaceByNumber(pb); 
	if ((InterfaceDescriptor == NULL) ||  (myErr != noErr))
	{
	  // report error and quit 
	}
			
	pb.usbBRequest = kHIDRqSetProtocol;
	pb.usbWValue = kHIDBootProtocolValue; 
	myErr = USBDeviceRequest(&pb);
	if (immediateError(myErr))
	{
	  // report error and quit 
	}
	break;
			
    case kSetIdleRequest:
	pb.usbBRequest = kHIDRqSetIdle;
	myErr = USBDeviceRequest(&pb);
	if(immediateError(myErr))
	{
        // report error and quit 
	}
	break;
     case kFindAndOpenInterruptPipe:
	pb.usbFlags = kUSBIn;
	pb.usbClassType = kUSBInterrupt;
	myErr = USBFindNextEndpointDescriptorImmediate( &pb );
	if((immediateError(myErr)) || (pb.usbBuffer == nil))
	{
	  // report error and quit 
	}
	else
	{
	  EndpointDescriptor = (USBEndPointDescriptorPtr)pb.usbBuffer;
	  …
	  myErr = USBOpenPipe( &pb );
	  if(immediateError(myErr))
	  {
    	// report error and quit 
	  }
	}
	break;
		
    case kReadInterruptPipe:
	…
	myErr = USBIntRead(&pb);
	if(immediateError(myErr))
	{
	  // report error and quit 
	}
	break;
			
    default:
	// report error and quit 
	break;
  }
	
  // At this point the control is returned to the system.  If a USB transaction
  // has been initiated, then it will call the Complete procs
  // (below) to handle the results of the transaction.
}
 


The InitializeInterfaceProc

USB calls the initializeInterfaceProc function to tell the class driver to prepare for communications with the USB interface. A composite USB device generally results in the load of the Composite class module. The Composite class module obtains information about configuration 0, verifies that the bus power required for the device is supported by the hub, then sets the configuration 0 as active. A call is made to NewInterfaceRef, where the USB expert searches for a class driver that supports interface matching and which best matches the interface that has been selected. This interface matching process is described previously.

The InitializeInterfaceProc routine is called synchronously at System Task Time. There is no error returned. The class driver allocates memory, sets up resources (see "Compatibility With Mac OS Toolbox" section below), and makes the necessary sequence of USB calls to configure the interface. This will include specifying the interface to be used, opening endpoints and pipes, specifying the protocol to be used, installing an interrupt routine, and related actions.

Before returning from the function, the driver must complete any calls that must be made at task time. After doing so, the driver should initiate an asynchronous "state machine process" to complete the connection. This can include memory allocation, assuming that the USBAllocMem call is made. Once the asynchronous state machine process is begun, the driver can return from the InitializeInterfaceProc function call.

As a reminder, most USB calls are asynchronous. Per the USB Reference Guide, the USB driver can NOT poll for completion of these asynchronous USB calls by simply checking the usbStatus field without using a completion routine.

All of the USB examples class drivers implement an asynchronous state machine startup process. In the examples, the last thing that the InitializeInterfaceProc code does before returning to the caller, is to make an InitiateTransaction call. The InitiateTransaction call uses the usbRefCon field as a selector into a switch statement. After the first call to InitiateTransaction is intiated, control returns to the caller. As the asynchronous call completes, the completion routine checks the call results, modifies the usbRefCon field appropriately and issues a call to InitiateTransaction. InitiateTransaction checks the usbRefCon field and issues a new asynchronous call to handle the next step in the state machine. This continues until all of the processing is complete.

The FinalizeProc

USB calls the finalizeProc function to tell the class driver that the device it was supporting has gone away and that the class driver is about to be unloaded. The finalizeProc is called at SystemTask time and is called synchronously. When the finalizeProc returns, the class module may be unloaded from memory. A crash can occur if the finalizeProc is called while there are USB calls which are pending completion. The crash occurs because immediately after returning from the finalizeProc, the class driver is unloaded from memory and the completion code may become invalid. To protect against this problem refer to the section on "Handling Hot Unplugging".

Difference between Device Drivers and Interface Drivers

The implementation of a Mac OS USB Class Driver is such that they may be loaded as either Device Drivers or Interface Drivers. The primary difference is that the Device Driver code must issue a SetConfiguration call (a USBDeviceRequest call with the usbBRequest field set to kUSBRqSetConfig) to define which configuration is active. In contrast, the Interface Driver can not set a configuration. Both types of drivers will identify the interfaces present, find one to activate, open the pipes in the desired interface, and handle I/O on the pipes within the interface.

Typically, the class driver is loaded as an interface driver if the device is detected as a composite class device and the composite driver is loaded. The Composite class driver detects the available configurations and makes the USBExpertInstallInterfaceDriver call, which results in the appropriate class module being loaded and initialized via the initializeInterfaceProc.

Handling Hot Unplugging

The class module must be designed to handle a "hot unplug" situation, where the client process still has the class driver open. When USB detects that a device has been unplugged, but before the finalizeProc is called, USB will call at interrupt time, the notificationProc, that was registered in the USBClassDriverPluginDispatchTable. The notificationProc is sent the message 0x0B, kNotifyDriverBeingRemoved. If the driver still has a client connection to maintain, then it can return the kUSBDeviceBusy, -6977, result. By returning the kUSBDeviceBusy result, USB will hold off calling the finalizeProc. USB will periodically call the notificationProc with the kNotifyDriverBeingRemoved message until the kUSBNoErr result is returned. Once this occurs, the finalizeProc is called, after which the driver code fragment is unloaded from memory.

This mechanism provides a means to protect your driver from being unloaded after the device has been removed. By continuing to return the kUSBDeviceBusy result, you can hold off the finalizeProc. Note that any calls into the USL to your device, will fail.

This raises the issue as to what happens to pending calls, when the device is unplugged. These calls may complete with error -6911, kUSBNotRespondingError, or they may be aborted by the pipe closure process with error -6982, kUSBAbortedError. Note that the class driver should never retry a kUSBAbortedError in the completion routine. The order of these errors or notifications is not guaranteed. They may occur after the notificationProc is called with the kNotifyDriverBeingRemoved.If this event occurs, then make the USBAbortPipeByReference call for each pipe with active transactions. The notificationProc must wait until all of the transactions have completed, before returning kUSBNoErr in response to the KNotifyDriverBeingRemoved message.If the kUSBNoErr response is returned with pending transactions incomplete, then the finalizeProc will be called, the class module will be unloaded, and the system crashes when the completion routine for the transaction finally completes at some point later on.

USBHIDModuleDispatchTable

A HID (Human Interface Device) Module, is required to implement and export an additional dispatch table in order to work under the Mac OS. Samples of the use of this structure, is presented in the source code for the MouseModule and for the Keyboard Module. The source code for these modules is presented in the Examples folder of the USB DDK. The USBHIDModuleDispatchTable is structured as follows:

struct USBHIDModuleDispatchTable {
	UInt32 				   hidDispatchVersion;
	USBHIDInstallInterruptProcPtr     pUSBHIDInstallInterrupt;
	USBHIDPollDeviceProcPtr 	    pUSBHIDPollDevice;
	USBHIDControlDeviceProcPtr 	 pUSBHIDControlDevice;
	USBHIDGetDeviceInfoProcPtr 	 pUSBHIDGetDeviceInfo;
	USBHIDEnterPolledModeProcPtr      pUSBHIDEnterPolledMode;
	USBHIDExitPolledModeProcPtr 	pUSBHIDExitPolledMode;
};

The USBHIDInstallInterruptProcPtr is used to install the interrupt routine that is called to process incoming data. The prototype for this function is defined as follows:

OSStatus USBHIDInstallInterruptProcPtr( HIDInterruptProcPtr HIDInterruptFunction, 
                                        UInt32 refcon);

When the class module is called via this dispatch proc pointer, the class module must save the HIDInterruptProcPtr and the refcon. When incoming data has been received, call the registered HIDInterruptProcPtr passing the saved refcon value as the first parameter, and the appropriate data structure pointer, as the second parameter. For keyboard devices, this is the USBKeyboardData structure. For mouse devices, this is the USBMouseData structure. The prototype for the HIDInterruptProcPtr function is defined as follows

void HIDInterruptProcPtr(UInt32 refcon, void *theData);
 

The USBHIDPollDeviceProcPtr is used to (just what is this function for) For the Mouse

The USBHIDControlDeviceProcPtr is used to have the device perform device specific functions. The prototype for this function is defined as follows:

OSStatus USBHIDControlDeviceProcPtr( UInt32 theControlSelector, void *theControlData);

The selector values which can be passed depend on the type of device. For mouse type HID devices, the selector values that can be expected are as follows

  • kHIDRemoveInterruptHandler - clear out the ShimInterruptHandler, as well as the associated refcon and save interrupt hanlder
  • kHIDEnableDemoMode - save the current interrupt handler, set up a CursorDevice Manager device, and install a data processing routine similar to the USBMouseIn routine from the file HIDEmulation.c from the Mouse Module example class driver. Refer to the USB DDK for this example.
  • kHIDDisableDemoMode - this call is made following a control call to kHIDEnableDemoMode. Dispose of the current CursorDevice Manager structure and replace the interrupt handler with the saved interrupt handler.

For keyboard type HID devices, the selector values that can be expected are as follows:

kHIDSetLEDStateByBits - command issued to device to set state of the keyboard LEDs. Refer to the KBDHIDEmulation.c file for the code on how this call should be handled.

kHIDRemoveInterruptHandler - clear out the current interrupt handler along with the refcon and any saved interrupt handler information.

kHIDEnableDemoMode - save the current interrupt routine and replace it with a version like the USBDemoKeyIn code. This routine is

The USBHIDGetDeviceInfoProcPtr is used to (to be completed later)

The USBHIDEnterPolledModeProcPtr is used to (to be completed later)

The USBHIDExitPolledModeProcPtr is used to (to be completed later)

Notes on Bulk Input/Output

Bulk Input/Output is handled via the USBBulkWrite and the USBBulkRead calls. For BulkRead calls, you can pass buffers that are larger than the "MaxPacketSize" value defined by the device. For example, supposed that the MaxPacketSize size is 64 bytes. You can pass a buffer of 4K, 16K or 64K to either of the USBBulk calls. In the case of USBBulkRead, the Open Host Controller Interface (OHCI ) will automatically issue bulk-in transfers until the device returns all of the requested bytes or it returns a short packet.

For USBBulkRead, you can queue several calls. USB guarantees that you will always process the BulkRead completion routines, in the order that you make the requests. This only applies on a per device basis. When the first USBRead call has completed, a second USBBulkRead call has already been queued for USB to process additional incoming data with.

Communicating With Client Processes

Overview

To this point, we have discussed how a Mac OS USB driver communicates with a USB device. Ultimately, there must be communications between the USB class driver and the Mac OS applications and processes. While there are specific guidelines for how USB drivers and devices communicate with each other, there is no official API for how client processes communicate with USB drivers. In this section we discuss different strategies for facilitating these communications.

The USB class driver is designed to be loaded and present to support an attached USB device. When the device is detached, the class driver is unloaded from memory. This makes the class driver unsuitable for client processes that are not prepared to handle a disappearing driver.

For most device IO, a device driver is implemented which presents a standard IO API to client processes. For serial communications and disk storage devices, this may mean that a DRVR style resource is loaded into memory with a corresponding unit entry in the device table. Keyboards, mice and graphics tablets typically register a driver with the ADB Manager. A camera implements a video digitizer, also known as a vdig. A printer requires a print chooser device. The key here, is to understand how existing, non-USB devices, that are similar to your USB device, currently communicate with Mac OS clients. You then develop a similar type of device driver, one which presents a familiar API to the client process, but which also knows how to communicate with the USB class module. For discussion purposes, let's call this modified driver, a "compatibility shim".

It's important that the "compatibility shim" not be implemented within the class driver. As mentioned previously, if a device is unplugged suddenly, then the class driver is called via the finalizeProc. Upon completion of the finalizeProc, the shared library driver code may be unloaded from memory. If the compatibility shim is included in the class driver, then this code also disappears.

In addition, with Mac OS X in the near future, it's important to isolate those portions of code that will require change in the near future. This is especially true of the "compatibility shim", which may require an API under the current OS, that will no longer be supported in the future. For this reason, we advise that you not implement the compatibility shim within the class module code.

In considering the design of a "compatibility shim", you should be familiar with the process by which application processes communicate with similar non-USB devices. In many cases, the compatibility shim should be thought of as a resident process that client processes can easily find. When the client process opens the "compatibility shim", the shim determines whether the USB device is attached and initiates communications with the class driver. The class driver knows about making calls to the USB device. The "compatibility shim" knows about communicating with the Mac OS.

If you will be designing a DRVR style driver, you will find the TradDriverLoadLibrary code invaluable. This code is available on the Developer CD, Tool Chest volume. The Developer CD also includes a RAMDisk sample that demonstrates the implementation of a working DRVR resource.

At some point, the compatibility shim finds the class driver, and implements communications between them. In the next section, we discuss the means by which the compatibility shim code can detect the presence of the class driver. One method for communicating between the two is for the class driver to export a dispatch table procptr. TheClassDriverPluginDispatchTable is an example of this mechanism. The USB Manager imports TheClassDriverPluginDispatchTable and calls the different procs as necessary. The class driver could define such a dispatch table and export it. The compatibility shim, would use the FindSym call to obtain the address of the dispatch table, then make the dispatch table calls as appropriate.

For some USB devices, there is no standard mechanism for communications with client processes. It is possible for an application to do exactly what the compatibility shim does. If an application does communicate directly with the USB class module driver, an important consideration concerns the hot-unplug situation. The application must be ready to handle this situation, just as the compatibility shim must be designed to do so also.

Compatibility Shim Info

In this section we point you to sample code which you will find helpful in designing the shim code.

For many compatibility shims, a 68K DRVR code resource may prove useful to implement. While this is legacy code and will not be supported under Mac OS X, it is the required mechanism for device Input/Output for many client processes.

For a USB Communications Class devices, the compatibility shim must provide a 68K driver and also register the DRVR with the Communications Toolbox (CTB) Comm Resource Manager. This allows the USB Communications device to be recognized by all CTB aware applications. For more information on the 68K DRVR mechanism, refer to Inside Macintosh: Devices.. A description of the standard process by which Mac OS applications find and open serial devices, is presented in Tech Note 1119, Serial Port Apocrypha.

For a sample of implementing a 68K 'DRVR' code resource, there is the RAMDisk v1.45 code sample. This sample is also useful in demonstrating how to set up a Metrowerks CodeWarrior project to create a DRVR code resource. The sample code demonstrates the use of the TradDriverLoaderLib code that facilitates the installation of all DRVR code resources into the Device Manager Device Unit Table.

For Video Input Devices, such as a USB camera, the standard mechanism for processing data from hardware, is the Video Digitizer. You can learn more Video Digitizers in Inside Macintosh: QuickTime Components, available from the QuickTime Developer Documentation Web Page.

For Networking Devices, such as an Ethernet/USB convertor, an Open Transport DLPI Driver must be implemented. As sample Loopback DLPI driver is provided as part of the Open Transport SDK. You can obtain the latest Open Transport SDK from the Apple Developer Open Transport Web Page.

For Print class devices, a compatibility shim would provide a Chooser Printer Driver. An example Chooser Printer Driver is provided with the USB DDK, in the Examples folder.

Detecting Device Presence

There are 2 different means for finding a USB device. Use the USBGetNextDeviceByClass call to check for the immediate presence of a device. There is the USBInstallDeviceNotification mechanism to be alerted when a device or interface is added or removed.

Use the USBGetNextDeviceByClass call to determine the presence of a device. This call returns the CFragConnectionID associated with the class module. Use the CfragConnectionID with the FindSym call to obtain the address of an exported symbol, for example the address to a procptr dispatch table. When making the USBGetNextDeviceByClass, you will want to set the input USBDeviceRef parameter to kNoDeviceRef so that it will find the first instance of a device. The following is an example of using this call where we look for a keyboard device. If the keyboard is found, then we look for the "TheHIDModuleDispatchTable" export symbol, which all HID class drivers are required to export.

UInt16  deviceClass = 0x0003;
UInt16  deviceSubClass = 0x0001;
UInt16  keyboardDeviceProtocol = 0x0001;
  CFragConnectionID  keyboardConnID;
CFragSymbolClass   symClass;
THz		    currentZone;
 currentZone = GetZone ();    // save the current zone for restoration
SetZone (SystemZone ());     // set current zone to system
status = USBGetConnectionIDByClass (&keyboardConnID, deviceClass,
						deviceSubClass, keyboardDeviceProtocol);
SetZone (currentZone);	 // restore the zone setting
	
if (status == noErr)
{
  currentZone = GetZone ();
  SetZone (SystemZone ());	
  status = FindSymbol (keyboardConnID, "\pTheHIDModuleDispatchTable", 
				(Ptr *)&pTheKeyboardDispatchTable, &symClass);
  SetZone (currentZone);
}
 

The significance of the sample above, is that it demonstrates a way for the compatibility shim code to find a procedure pointer table defined by the class module. This mechanism also demonstrates a limitation for writing universal handlers in that all other devices of the same type would need to implement the exact same procedure pointer tables and functionality.

For compatibility shims that need to handle "hot plug-in" device connections, there is a mechanism for notification of AddDevice. AddInterface, RemoveDevice, and RemoveInterface events. Use the USBInstallDeviceNotification call to install a notification handler.

The following sample code demonstrates the use of the USBInstallDeviceNotification call. Note that the sample is designed to have the notification routine called whenever any USB device is plugged in or unplugged from the USB chain.

pb.usbDeviceNotification = -1;	// tell me about everything
pb.usbClass = kUSBHIDInterfaceClass; // want to know about HID class devices
pb.usbSubClass = -1;	          // tell me about all sublclass devices
pb.usbProtocol = -1;                 // don't care about the protocol used by device
pb.usbVendor = -1;	            // we'll allow any vendors keyboard to notify us
pb.usbProduct = -1;	           // allow any product ID to notify us
pb.result = noErr;
pb.callback = (USBDeviceNotificationCallbackProcPtr)&myNotificationCallback;		
pb.refcon = nil;
 USBInstallDeviceNotification (&pb);	

For this notification sample described above, the notification proc might be as shown below.

OSStatus ShimOpenDriver(USBDeviceRef theDevRef)
{
  OSStatus		theErr = 0;
  CFragSymbolClass       symClass;
  CFragConnectionID      connID;
  THz		     currentZone;
   theDevRef = 0;
  currentZone = GetZone();         // save the current zone setting to restore to later
  SetZone (SystemZone ());         // set the current zone to the system zone
                                   // look for the desired device
  theErr = USBGetNextDeviceByClass(&theDevRef, &connID, 
						kUSBInterestingClass, 0, 0);
  SetZone (currentZone);           // restore the zone
   if (theErr == noErr)
  {
    SetZone(SystemZone());         // set the current zone to the system zone
                                   // find the desired exported symbol
    theErr = FindSymbol(connID, "\pTheModuleDispatchTable", 
				(Ptr *)&pTheDispatchTable, &symClass);
 
    SetZone (currentZone);         // restore the zone
  }	
  return theErr;
}
 void myNotificationCallback(USBDeviceNotificationParameterBlock *pb)
{
  switch(pb->usbDeviceNotification) // why were we notified?
  {
    case kNotifyAddInterface:	// because a HID device appeared
	ShimOpenDriver(pb->usbDeviceRef);  
	break;
			
    case kNotifyRemoveInterface:   // because a HID device disappeared
	break;
			
    default:
	break;
  }
}

The format of the exported symbol TheModuleDispatchTable would be specific for use between the shim and class module. Some elements of the dispatch table might include a notification procptr, and other procptrs to handle read, write, control, status and killio requests from the client.

USBNotification Procedure Callback Messages

The callback messages include:

Client notification messages

  • KNotifyAddDevice - USB device driver has been loaded
  • KNotifyRemoveDevice - USB device driver is about to be removed. After this message is processed, the finalizeProc for the device is called.
  • KNotifyAddInterface - USB interface driver has been loaded
  • KRemoveInterface - USB device driver that was loaded as an interface driver, is about to be removed. After this message is processed, the finalizeProc for the interface is called.

The following messages are mentioned in the USB.h header file, but are only used internally.

    KNotifyGetNextDeviceByClass
    KNotifyGetDriverConnectionByID
    KNotifyInstallDeviceNotification
    KNotifyRemoveDeviceNotification
    KNotifyDeviceRefToBusRef
    KNotifyPowerState
    KNotifyStatus
    KNotifyFatalError

Mac OS USB Compatibility With Mac OS Toolbox Calls

The use of Mac OS Toolbox calls is discouraged from within a USB class driver, however, there are cases when the Toolbox calls can be made. Carefully consider whether a Toolbox call is really required within the class driver, as opposed to implementing the functionality within the compatibility shim. There are cases where the use of Toolbox calls make sense to handle concerns that USB Manager may not be able to handle.

It is important to understand that the class driver operates at deferred task time, under interrupt conditions. Many of the Toolbox calls cannot be made in an interrupt context. The exception to this guideline is that the intializeProc and the finalizeProc are called at system task time.

With regards to memory allocation, the use of USBAllocMem is preferable to the NewPtrSys call. The USBAllocMem call is designed to use an appropriate memory allocation method for the system software releases. The NewPtrSys call may be supported under the current implementation of USB, but it may not be in the future. Note that one can make the NewPtrSys call during the intializeProc.

To handle preferences, the preferred solution is to have the compatibility shim access the Resource Manager, then tell the class driver to implement the setting via the ControlProc. The ControlProc would be a proc imported to the compatibility shim as described above.

The 1.0 release of Mac OS USB does not provide timeout support for USB calls. Where this is a requirement, the class driver could implement a "watchdog" Time Mananger task, to check if a USB call has completed in a specified period of time. Examples of using the Time Manager can be found in "Inside Macintosh: Processes", Chapter 3, Time Manager.

 

Mac OS USB Notes

The following notes may be of use regarding the Mac OS USB DDK.

1. Control transactions (using USBDeviceRequest) are not re-entrant. If USBDeviceRequest is re-entered at a critical time it can fail in strange ways. The expert log may have messages along the lines of "UIM Interface odd completion" and "UIM Interface spare transactions". You may also experience multiple call backs or possibly no call backs.

This situation should never arise if only one thread of execution accesses a device, or if all control accesses are done at secondary interrupt time. USL callbacks are normally at secondary interrupt time so for state machine drivers this is not normally a problem. Calling the USL from an application can cause this problem to arise. We're working on a fix, but the currently available DDK's and CPUs all have his problem.

 

2. Aborting a Pipe (USBAbortPipeByReference or USBResetPipeByReference) should result in your transactions being returned synchronously (which is why those routines are not async). However, if for any reason the completion routine of an outstanding transaction isn't called immediately (say the transaction has already been processed by the HW, but the secondary interrupt to notify you hasn't happened yet), you should not assume that transaction is aborted, and instead wait for it to complete.

Any re-use of the paramblock could result in the completion routine being called twice or the "odd completion" messages. One programming error, which leads to this problem, stems from a missing "break" statement in a Switch statement/statemachine, which could result in multiple request using the same paramblock.

Do not reuse a data buffer until a completion routine is called (either by normal completion, or with an AbortedError). Until that time, the HW could still access your data.

 

3. We have identified a problem with the use of the USBBulkRead call which can result in a -6908 error, kUSBOverRunErr. This happens whether the buffer is " MaxPacketSize " in size or much larger. The problem results if the buffer does not begin on a " MaxPacketSize " aligned address. For Control and Bulk packets, the " MaxPacketSize " value can be 64/32/16/8 bytes.

The following code example demonstrates a solution to this problem - setting the usbBuffer field to a buffer that is on a "MaxPacketSize" aligned address. Assuming that we want to pass in a buffer that is of size "MaxPacketSize", the following algorithm demonstrates how to set up the usbBuffer so that it is aligned correctly. Note we are working with logical, not physical addresses. Also note that there is no requirement on the size of the buffer except that it should be larger than "MaxPacketSize".

UInt16 maxpacksize = 64; // set for MaxPacketSize of 64 bytes
UInt8 *buffer;
UInt8 *alignedBuffer;
 // first allocate a buffer that is the desired
// 2 * maxpacksize into System heap
 buffer = NewPtrSys(2 * maxpacksize);
 // figure out where the buffer falls on the
// maxpacksize aligned address
 alignedbuffer = (buffer + maxpacksize) & ~(maxpacksize - 1);
 //Following this example, you would set
 usbReqCount = maxpacksize;
usbBuffer = alignedBuffer;

The sample method for allocating a buffer wastes "MaxPacketSize" amount of memory. A better method would be to allocate a larger chunk of memory and divide the memory into " MaxPacketSize " size chunks that are used with different USBBulkRead calls. Note also that there is no requirement that the buffer must be a multiple of MaxPacketSize in size. The requirement is that the buffer must be aligned to a "MaxPacketSize" address.

The above workaround solution means that the class driver will have to double buffer incoming data. In a release of USB after v1.0, this problem will be fixed.

 

4. The following is sample code for accessing a string descriptor via the kUSBRqGetDescriptor call.

#include <Types.h>#include <Memory.h>#include <Events.h>#include <DriverServices.h>#include <USB.h> #define TIMEOUT 60
 void ProbeEndpointCompletionRoutine(USBPB *usbpb);
void ExpertNumToString(SInt32 num, char *str);
OSStatus GetStringDescriptor(USBDeviceRef ref, UInt16 index,
UInt16 len, UInt8 *buf);
extern void USBIdleTask(void);
 void ProbeEndpointCompletionRoutine(USBPB *usbpb)
{
  if (usbpb->usbStatus == kUSBPending)
  {
    // DebugStr("\pcompletition called with pending status!");
    usbpb->usbStatus = paramErr;
  }
  usbpb->usbRefcon = false;
}
 // This is a simple implementation of numtostring,
// This version is fully reentrant
void ExpertNumToString(SInt32 num, char *str)
{
  char buf[16];
  SInt16 cnt=0;
   if (num < 0){
    num = -num;
    *str++ = '-';
  }
  do{
    buf[cnt++] = "0123456789"[num % 10];
    num = num/10;
  } while (num);
   while (cnt-- > 0){
    *str++ = buf[cnt];
  }
  *str = '\0';
}
 //-------------------------------
//
//GetStringDescriptor
//
// Descriptor type == 3
//-------------------------------
OSStatus GetStringDescriptor(USBDeviceRef ref, UInt16 index, UInt16 len, UInt8 *buf)
{
  UInt32    startTime;
  OSStatus  status;
  USBPB     *pb;
  char      theText[255] = "";
  char      tempStr[8] = "";
   pb = (USBPB *)NewPtrSysClear(sizeof(*pb));
  if (!pb)
    return(memFullErr);
     // set up to make the kUSBRqGetDescriptor call
  pb->pbLength = (sizeof(*pb));
  pb->usbReference = ref;
  pb->usbRefcon = true;
  pb->usbCompletion = ProbeEndpointCompletionRoutine;
  pb->pbVersion = kUSBCurrentPBVersion;
  pb->usbBMRequestType = USBMakeBMRequestType(kUSBIn, kUSBStandard,
                                              kUSBDevice);
  pb->usbBRequest = kUSBRqGetDescriptor;
  pb->usbWValue = 0x0300 | index;
  pb->usbWIndex = 0x409;
   /* First get the string length in preparation for the real call */
  pb->usbReqCount = 2; // 2-byte length for Unicode strings
  pb->reserved4 = len;
  pb->usbBuffer = buf;
  *buf = 0; // in case we don't get anything back!
   startTime = TickCount();
  status = USBDeviceRequest(pb);
   if (status && (status != kUSBPending))
  { // there was an immediate error - bail!
    DisposePtr((void*)pb);
    return(status);
  }
   while(pb->usbRefcon /*pb->usbStatus == kUSBPending*/){
    USBIdleTask();
    if (pb->usbRefcon && ((startTime+TIMEOUT) < TickCount())){
      pb->usbRefcon = false; // we got tired of waiting for a response
      pb->usbStatus = -1;
    }
  }
  if (pb->usbStatus > 0 )
    pb->usbStatus = noErr;
   /* Now make the real call */
  if (pb->usbStatus == noErr)
  {
    pb->pbLength = (sizeof(*pb));
    pb->usbReference = ref;
    pb->usbRefcon = true;
    pb->usbCompletion = ProbeEndpointCompletionRoutine;
    pb->pbVersion = kUSBCurrentPBVersion;
    pb->usbBMRequestType = USBMakeBMRequestType(kUSBIn, kUSBStandard,
                                                kUSBDevice);
    pb->usbBRequest = kUSBRqGetDescriptor;
    pb->usbWValue = 0x0300 | index;
    pb->usbWIndex = 0x409;
    pb->usbReqCount = (UInt32)*buf; // actual data length;
    pb->reserved4 = len;
    pb->usbBuffer = buf;
    *buf = 0; // in case we don't get anything back!
     startTime = TickCount();
    status = USBDeviceRequest(pb);
     if (status && (status != kUSBPending))
    { // there was an immediate error - bail!
      DisposePtr((void*)pb);
      return(status);
    }
     while(pb->usbRefcon /*pb->usbStatus == kUSBPending*/){
      USBIdleTask();
      if (pb->usbRefcon && ((startTime+TIMEOUT) < TickCount())){
        pb->usbRefcon = false; // we got tired of waiting for a response
        pb->usbStatus = -1;
      }
    }
    if (pb->usbStatus > 0 )
    pb->usbStatus = noErr;
  }
   status = pb->usbStatus;
  if (!status)
  {
    DisposePtr((void*)pb);
  }
  return(status);
}

 

Summary

Mac OS USB provides an API set for the implementation of class drivers for interrupt and bulk transfer type devices. When Isochronous support is implemented, this Tech Note will be updated to reflect that fact. Developers should find that the implementation of the USB Class Driver itself is a straightforward process. The programming challenge lies with the implementation of the compatibility shim that will be used to connect the data input/output flow between the device and Mac OS client processes. By knowing how client processes communicate with the same non-USB devices, one can implement a compatibility shim to fit between the client processes and the USB Class Driver.


Further References

Thanks to Craig Keithley, Dave Ferguson, Barry Twycross, Steve Schwander, Mike Shebanek, and Guillermo Gallegos, for their invaluable assistance and comments in writing this Tech Note.